# coding=utf-8
# pynput
# Copyright (C) 2015-2024 Moses Palmér
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The mouse implementation for *Windows*.
"""

# pylint: disable=C0111
# The documentation is extracted from the base classes

# pylint: disable=R0903
# We implement stubs

import ctypes
import enum

from ctypes import (
    windll,
    wintypes)

from pynput._util import NotifierMixin
from pynput._util.win32 import (
    INPUT,
    INPUT_union,
    ListenerMixin,
    MOUSEINPUT,
    SendInput,
    SystemHook)
from . import _base

#: A constant used as a factor when constructing mouse scroll data.
WHEEL_DELTA = 120


class Button(enum.Enum):
    """The various buttons.
    """
    unknown = None
    left = (MOUSEINPUT.LEFTUP, MOUSEINPUT.LEFTDOWN, 0)
    middle = (MOUSEINPUT.MIDDLEUP, MOUSEINPUT.MIDDLEDOWN, 0)
    right = (MOUSEINPUT.RIGHTUP, MOUSEINPUT.RIGHTDOWN, 0)
    x1 = (MOUSEINPUT.XUP, MOUSEINPUT.XDOWN, MOUSEINPUT.XBUTTON1)
    x2 = (MOUSEINPUT.XUP, MOUSEINPUT.XDOWN, MOUSEINPUT.XBUTTON2)


class Controller(NotifierMixin, _base.Controller):
    __GetCursorPos = windll.user32.GetCursorPos
    __SetCursorPos = windll.user32.SetCursorPos

    def __init__(self, *args, **kwargs):
        super(Controller, self).__init__(*args, **kwargs)

    def _position_get(self):
        point = wintypes.POINT()
        if self.__GetCursorPos(ctypes.byref(point)):
            return (point.x, point.y)
        else:
            return None

    def _position_set(self, pos):
        pos = int(pos[0]), int(pos[1])
        self.__SetCursorPos(*pos)
        self._emit('on_move', *pos)

    def _scroll(self, dx, dy):
        if dy:
            SendInput(
                1,
                ctypes.byref(INPUT(
                    type=INPUT.MOUSE,
                    value=INPUT_union(
                        mi=MOUSEINPUT(
                            dwFlags=MOUSEINPUT.WHEEL,
                            mouseData=int(dy * WHEEL_DELTA))))),
                ctypes.sizeof(INPUT))

        if dx:
            SendInput(
                1,
                ctypes.byref(INPUT(
                    type=INPUT.MOUSE,
                    value=INPUT_union(
                        mi=MOUSEINPUT(
                            dwFlags=MOUSEINPUT.HWHEEL,
                            mouseData=int(dx * WHEEL_DELTA))))),
                ctypes.sizeof(INPUT))

        if dx or dy:
            px, py = self._position_get()
            self._emit('on_scroll', px, py, dx, dy)

    def _press(self, button):
        SendInput(
            1,
            ctypes.byref(INPUT(
                type=INPUT.MOUSE,
                value=INPUT_union(
                    mi=MOUSEINPUT(
                        dwFlags=button.value[1],
                        mouseData=button.value[2])))),
            ctypes.sizeof(INPUT))

    def _release(self, button):
        SendInput(
            1,
            ctypes.byref(INPUT(
                type=INPUT.MOUSE,
                value=INPUT_union(
                    mi=MOUSEINPUT(
                        dwFlags=button.value[0],
                        mouseData=button.value[2])))),
            ctypes.sizeof(INPUT))


@Controller._receiver
class Listener(ListenerMixin, _base.Listener):
    #: The Windows hook ID for low level mouse events, ``WH_MOUSE_LL``
    _EVENTS = 14

    WM_LBUTTONDOWN = 0x0201
    WM_LBUTTONUP = 0x0202
    WM_MBUTTONDOWN = 0x0207
    WM_MBUTTONUP = 0x0208
    WM_MOUSEMOVE = 0x0200
    WM_MOUSEWHEEL = 0x020A
    WM_MOUSEHWHEEL = 0x020E
    WM_RBUTTONDOWN = 0x0204
    WM_RBUTTONUP = 0x0205
    WM_XBUTTONDOWN = 0x20B
    WM_XBUTTONUP = 0x20C

    MK_XBUTTON1 = 0x0020
    MK_XBUTTON2 = 0x0040

    XBUTTON1 = 1
    XBUTTON2 = 2

    #: A mapping from messages to button events
    CLICK_BUTTONS = {
        WM_LBUTTONDOWN: (Button.left, True),
        WM_LBUTTONUP: (Button.left, False),
        WM_MBUTTONDOWN: (Button.middle, True),
        WM_MBUTTONUP: (Button.middle, False),
        WM_RBUTTONDOWN: (Button.right, True),
        WM_RBUTTONUP: (Button.right, False)}

    #: A mapping from message to X button events.
    X_BUTTONS = {
        WM_XBUTTONDOWN: {
            XBUTTON1: (Button.x1, True),
            XBUTTON2: (Button.x2, True)},
        WM_XBUTTONUP: {
            XBUTTON1: (Button.x1, False),
            XBUTTON2: (Button.x2, False)}}

    #: A mapping from messages to scroll vectors
    SCROLL_BUTTONS = {
        WM_MOUSEWHEEL: (0, 1),
        WM_MOUSEHWHEEL: (1, 0)}

    _HANDLED_EXCEPTIONS = (
        SystemHook.SuppressException,)

    class _MSLLHOOKSTRUCT(ctypes.Structure):
        """Contains information about a mouse event passed to a ``WH_MOUSE_LL``
        hook procedure, ``MouseProc``.
        """
        _fields_ = [
            ('pt', wintypes.POINT),
            ('mouseData', wintypes.DWORD),
            ('flags', wintypes.DWORD),
            ('time', wintypes.DWORD),
            ('dwExtraInfo', ctypes.c_void_p)]

    #: A pointer to a :class:`_MSLLHOOKSTRUCT`
    _LPMSLLHOOKSTRUCT = ctypes.POINTER(_MSLLHOOKSTRUCT)

    def __init__(self, *args, **kwargs):
        super(Listener, self).__init__(*args, **kwargs)
        self._event_filter = self._options.get(
            'event_filter',
            lambda msg, data: True)

    def _handle(self, code, msg, lpdata):
        if code != SystemHook.HC_ACTION:
            return

        data = ctypes.cast(lpdata, self._LPMSLLHOOKSTRUCT).contents

        # Suppress further propagation of the event if it is filtered
        if self._event_filter(msg, data) is False:
            return

        if msg == self.WM_MOUSEMOVE:
            self.on_move(data.pt.x, data.pt.y)

        elif msg in self.CLICK_BUTTONS:
            button, pressed = self.CLICK_BUTTONS[msg]
            self.on_click(data.pt.x, data.pt.y, button, pressed)

        elif msg in self.X_BUTTONS:
            button, pressed = self.X_BUTTONS[msg][data.mouseData >> 16]
            self.on_click(data.pt.x, data.pt.y, button, pressed)

        elif msg in self.SCROLL_BUTTONS:
            mx, my = self.SCROLL_BUTTONS[msg]
            dd = wintypes.SHORT(data.mouseData >> 16).value // WHEEL_DELTA
            self.on_scroll(data.pt.x, data.pt.y, dd * mx, dd * my)
